/**
 * Copyright (C) 2011 Andrew C. Love (DNC) <dnc.app.sup@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dnc.cloak.framework.security.encryption;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.Logger;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.x509.X509V3CertificateGenerator;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

public class EncryptionUtil implements EncryptionProperties {
    private static final Logger logger = Logger.getLogger(EncryptionUtil.class.getName());

    private static Hashtable<String,String> providerNames = new Hashtable<String,String>();

    /*
     * Return the name of a provider
     */
    public static String getProviderName(String providerClassName) {
        if (!providerNames.contains(providerClassName)) {
            setupProvider(providerClassName);
        }

        return (String) providerNames.get(providerClassName);
    }

    /*
     * Setup a provider for encryption
     */
    public static Provider getProvider(String providerClassName) {
        if (!providerNames.contains(providerClassName)){
            setupProvider(providerClassName);
        }

        Provider[] providers = Security.getProviders();

        Provider provider = null;
        boolean foundProvider = false;
        for (int i = 0; i != providers.length; i++) {
            if (providers[i].getClass().getName().equalsIgnoreCase(providerClassName)) {
                foundProvider = true;
                provider = providers[i];
                if (logger.isDebugEnabled()) {
                    logger.debug(providers[i].getName() + " found");
                }
                break;
            }
        }

        if (!foundProvider) {
            logger.error(providerClassName + " not found!");
        }

        return provider;
    }

    /*
     * Setup a provier for encryption
     */
    public static synchronized void setupProvider(String providerClassName) {
        // Retrieve the Provider
        Provider provider = null;

        if (providerClassName == null) {
            providerClassName = DEFAULT_JCE_PROVIDER;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("providerClassName" + "=" + providerClassName);
            logger.debug(listProviders());
        }

        // Check if the provider is already installed
        if (providerNames.get(providerClassName) != null)
            return;

        Provider[] providers = Security.getProviders();

        boolean foundProvider = false;
        for (int i = 0; i != providers.length; i++) {
            if (providers[i].getClass().getName().equalsIgnoreCase(providerClassName)) {
                foundProvider = true;
                provider = providers[i];
                if (logger.isDebugEnabled()) {
                    logger.debug(providers[i].getName() + " already installed");
                }
                break;
            }
        }

        if (foundProvider) {
            providerNames.put(providerClassName, provider.getName());
            return;
        }

        try {
            provider = (Provider) (Class.forName(providerClassName).newInstance());
            if (logger.isDebugEnabled()) {
                logger.debug("providerClassName" + "=" + providerClassName);
            }
        }
        catch (Exception ex) {
            logger.error("Could not instantiate JCE Provider->" +  providerClassName + ":" + ex.toString());
            logger.error("Could not instantiate JCE Provider->" + providerClassName, ex);
        }
        
        int securityPosition = Security.addProvider(provider);

        if (securityPosition < 0) {
            logger.error("Could not add JCE Provider->" +  providerClassName);
        }
        else {
            providerNames.put(providerClassName, provider.getName());
            logger.info("Added JCE Provider->" + provider.getName() + "=" + providerClassName + " at index:" + securityPosition);
        }

    }

    /**
     * List the available algorithm names for ciphers, key agreement, macs,
     * message digests and signatures.
     */
    public static String listProviders() {
        Provider[] providers = Security.getProviders();
        Set<String> ciphers = new HashSet<String>();
        Set<String> keyAgreements = new HashSet<String>();
        Set<String> macs = new HashSet<String>();
        Set<String> messageDigests = new HashSet<String>();
        Set<String> signatures = new HashSet<String>();

        StringBuffer output = new StringBuffer();
        for (int i = 0; i != providers.length; i++) {
            Iterator<Object> it = providers[i].keySet().iterator();

            output.append("Provider[" + i + "]:" + providers[i].getName() + ":" + providers[i].getClass().getName());

            while (it.hasNext()) {
                String entry = (String) it.next();

                if (entry.startsWith("Alg.Alias.")) {
                    entry = entry.substring("Alg.Alias.".length());
                }

                if (entry.startsWith("Cipher.")) {
                    ciphers.add(entry.substring("Cipher.".length()));
                }
                else
                    if (entry.startsWith("KeyAgreement.")) {
                        keyAgreements.add(entry.substring("KeyAgreement.".length()));
                    }
                    else
                        if (entry.startsWith("Mac.")) {
                            macs.add(entry.substring("Mac.".length()));
                        }
                        else
                            if (entry.startsWith("MessageDigest.")) {
                                messageDigests.add(entry.substring("MessageDigest.".length()));
                            }

                            else
                                if (entry.startsWith("Signature.")) {
                                    signatures.add(entry.substring("Signature.".length()));
                                }
            }
            output.append(printSet("Ciphers", ciphers));
            output.append(printSet("KeyAgreeents", keyAgreements));
            output.append(printSet("Macs", macs));
            output.append(printSet("MessageDigests", messageDigests));
            output.append(printSet("Signatures", signatures));

            ciphers.clear();
            keyAgreements.clear();
            macs.clear();
            messageDigests.clear();
            signatures.clear();
        }
        return output.toString();
    }

    /**
     * Convert a byte to an unsigned integer. Java does not support unsigned
     * integers. The bitwise && with 0xFF removes the sign bit
     * 
     * @param b
     *            The byte to convert
     * @return
     */
    public static int unsignedByteToInt(byte b) {
        return b & 0xFF;
    }

    /**
     * Returns a hex string for a byte
     * 
     * @param value
     *            The byte to convert
     * @param length
     *            The length of the hex string to return
     * @return
     */
    public static String getHexString(byte value, int length) {
        return getHexString(EncryptionUtil.unsignedByteToInt(value), length);
    }

    /**
     * Returns a hex string for an integer
     * 
     * @param value
     *            The byte to convert
     * @param length
     *            The length of the hex string to return
     * @return
     */
    public static String getHexString(int value, int length) {
        String hex = Integer.toHexString(value);
        int hexlength = hex.length();

        if (hexlength < length) {
            for (int i = 0; i < length - hexlength; i++)
                hex = "0" + hex;
        }
        else {
            if (hexlength > length) {
                hex = hex.substring(0, 4);
            }
        }

        return hex.toUpperCase();
    }

    /**
     * Get a byte from a hex string. The byte is defined by two characters hex =
     * 1 byte byte index is 0 based
     * 
     * @param hexString
     * @param byteIndex
     * @return
     */
    public static byte getByteFromHexString(String hexString, int byteIndex) {
        return (byte) Integer.parseInt(hexString.substring(2 * byteIndex, (2 * byteIndex) + 2), 16);
    }

    /**
     * Convert an array of bytes to an integer
     * 
     * @param rawData
     * @return
     */
    public static int getUnsignedIntFromByteArray(byte[] rawData) {
        int value = 0;
        value += unsignedByteToInt(rawData[0]) << 24;
        value += unsignedByteToInt(rawData[1]) << 16;
        value += unsignedByteToInt(rawData[2]) << 8;
        value += unsignedByteToInt(rawData[3]) << 0;
        return value;
    }

    /**
     * Display the values in a set
     * 
     * @param setName
     * @param algorithms
     * @return
     */
    public static String printSet(String setName, Set<String> algorithms) {
        String output = "\t" + setName + ":";

        if (algorithms.isEmpty()) {
            output += "\t\tNone available.";
        }
        else {
            Iterator<String> it = algorithms.iterator();

            while (it.hasNext()) {
                String name = (String) it.next();

                output += ("\t\t" + name);
            }
        }

        return output;
    }

    /**
     * Base64 encode the data
     * 
     * @param data
     * @return
     * @throws Exception
     */
    public static String base64Encode(byte[] data) {
        String encodedData = new BASE64Encoder().encode(data);
        encodedData = encodedData.replaceAll("\r", "");
        encodedData = encodedData.replaceAll("\n", "");
        return encodedData;
    }

    /**
     * Base64 decode the data
     * 
     * @param data
     * @return
     * @throws Exception
     */
    public static byte[] base64Decode(String data) throws EncryptionException{
        try {
			return new BASE64Decoder().decodeBuffer(data);
		} 
        catch (IOException ex) {
			throw new EncryptionException(ex);
		}
    }

    /**
     * URL encode the data
     * 
     * @param data
     * @return
     * @throws Exception
     */
    public static String urlEncode(String data) throws EncryptionException {
        try {
			return URLEncoder.encode(data, "UTF-8");
		} 
        catch (UnsupportedEncodingException ex) {
        	throw new EncryptionException(ex);
		}
    }

    /**
     * URL decode the data
     * 
     * @param data
     * @return
     * @throws Exception
     */
    public static String urlDecode(String data) throws EncryptionException {
        try {
			return URLDecoder.decode(data, "UTF-8");
		}
        catch (UnsupportedEncodingException ex) {
        	throw new EncryptionException(ex);
		}

    }

    /**
     * Generate a token from the public key
     * 
     * @param publicKey
     * @return
     */
    public static String generateToken(PublicKey publicKey) {
        try {
            return base64Encode(publicKey.getEncoded());
        }
        catch (Exception ex) {
            return new Date().getTime() + "";
        }
    }
    
    public static Properties getDefaultJCEProperties() {
        Properties jceProperties = new Properties();
        jceProperties.put(PASSWD_PROPS_ENCRYPTED, DEFAULT_PASSWD_PROPS_ENCRYPTED);
        jceProperties.put(KEYSTORE_TYPE, DEFAULT_KEYSTORE_TYPE);
        jceProperties.put(KEYSTORE, DEFAULT_KEYSTORE);
        jceProperties.put(KEYSTORE_PASS, DEFAULT_KEYSTORE_PASS);
        jceProperties.put(KEY_ALIAS, DEFAULT_KEY_ALIAS);
        jceProperties.put(KEY_PASS, DEFAULT_KEY_PASS);
        jceProperties.put(SYM_ALGORITHM, DEFAULT_SYM_ALGORITHM);
        jceProperties.put(SYM_TRANSFORMATION, DEFAULT_SYM_TRANSFORMATION);
        jceProperties.put(PKI_ALGORITHM, DEFAULT_PKI_ALGORITHM);
        jceProperties.put(PKI_TRANSFORMATION, DEFAULT_PKI_TRANSFORMATION);
        jceProperties.put(MAX_LENGTH, (DEFAULT_MAX_LENGTH + ""));
        jceProperties.put(JCE_PROVIDER, DEFAULT_JCE_PROVIDER);
        jceProperties.put(SYM_KEY_INIT_SIZE, DEFAULT_SYM_KEY_INIT_SIZE + "");
        jceProperties.put(CACHE_CLIENT_CERT, DEFAULT_CACHE_CLIENT_CERT);
        jceProperties.put(CACHE_MAX_DURATION, DEFAULT_CACHE_MAX_DURATION);
        jceProperties.put(SIGNATURE_ALGORITHM, DEFAULT_SIGNATURE_ALGORITHM);
        jceProperties.put(PKI_KEY_INIT_SIZE, DEFAULT_PKI_KEY_INIT_SIZE + "");
        jceProperties.put(CERT_DISTINGUISHED_NAME, DEFAULT_DISTINGUISHED_NAME);
        jceProperties.put(CERT_DAYS_VALID, DEFAULT_CERT_DAYS_VALID + "");
        jceProperties.put(CERT_NEW_ON_EXPIRE, DEFAULT_CERT_NEW_ON_EXPIRE );

        return jceProperties;

    }
    
    public static String generatePassword(int length) {

        StringBuffer generatedPassword = new StringBuffer();
        SecureRandom random = new SecureRandom();

        for (int i = 0; i < length; i++) {
            // 32-126 are human readable
            int randomValue = 32 + random.nextInt(94);
            char c = (char) (randomValue);
            generatedPassword.append(c);
        }

        return generatedPassword.toString();

    }
    
    public static KeyStore createKeyStore(String providerClassName, String keyStoreType, String x509DistinguishedName, String signatureAlgorithm, String alias, String algorithm, int certDaysValid, int keySize, String keyStorePassword,String keyStoreEntryPassword) throws EncryptionException {

        try {
			Provider provider = EncryptionUtil.getProvider(providerClassName);

			KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(algorithm, provider);
			keyGenerator.initialize(keySize);

			KeyPair key = keyGenerator.generateKeyPair();
			PublicKey publicKey = key.getPublic();
			PrivateKey privateKey = key.getPrivate();
			
            KeyStore keyStore = null;
			keyStore = KeyStore.getInstance(keyStoreType);
			keyStore.load(null, keyStorePassword.toCharArray());

            X509Certificate certificate = createCertificate(x509DistinguishedName, signatureAlgorithm, provider, publicKey, privateKey, certDaysValid);
			X509Certificate[] certificateChain = new X509Certificate[] { certificate };
			keyStore.setCertificateEntry(alias, certificate);
            
			keyStore.setKeyEntry(alias, privateKey, keyStoreEntryPassword.toCharArray(), certificateChain);

            return keyStore;
		} 
        catch (NoSuchAlgorithmException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (KeyStoreException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (CertificateException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (IOException ex) {
        	throw new EncryptionException(ex);
		}
    }    
    
    public static X509Certificate createCertificate(String x509DistinguishedName, String signatureAlgorithm, Provider provider, PublicKey publicKey, PrivateKey privateKey, int certDaysValid) throws EncryptionException  {
       
        try {
			X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
			X509Name x509Name = new X509Name(true, x509DistinguishedName);
			certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));

			certGen.setIssuerDN(x509Name);
			certGen.setNotBefore(new Date());
			Calendar cal = Calendar.getInstance();
			cal.setTime(new Date());
			cal.add(Calendar.DATE, certDaysValid);
			certGen.setNotAfter(cal.getTime());

			certGen.setSubjectDN(x509Name);
			certGen.setPublicKey(publicKey);
			certGen.setSignatureAlgorithm(signatureAlgorithm);

			// No need for these right now, but keep here as a placeholder
			// certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false,new
			// AuthorityKeyIdentifierStructure(caCert));
			// certGen.addExtension(X509Extensions.SubjectKeyIdentifier, false,new
			// SubjectKeyIdentifierStructure(key.getPublic()));
			return certGen.generate(privateKey, provider.getName());
		} 
        catch (CertificateEncodingException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (InvalidKeyException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (IllegalArgumentException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (IllegalStateException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (NoSuchProviderException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (NoSuchAlgorithmException ex) {
        	throw new EncryptionException(ex);
		} 
        catch (SignatureException ex) {
        	throw new EncryptionException(ex);
		}
    }   
    
    public static KeyStore loadKeyStoreFromFile(String KeyStoreFileName, String providerClassName, String keyStoretype, String keyStorePassword) throws EncryptionException {
        InputStream keyStoreIS = null;
    
        try {
            EncryptionUtil.setupProvider(providerClassName);
        }
        catch (Exception ex) {
            logger.error("Could not instantiate JCE Provider->" + providerClassName + ":" + ex.toString());
            logger.error("Could not instantiate JCE Provider->" + providerClassName, ex);
        }

        try {
            keyStoreIS = new FileInputStream(new File(KeyStoreFileName));
        }
        catch (FileNotFoundException e) {
            throw new EncryptionException(e);
        }
        
        return loadKeyStorefromStream(keyStoreIS, providerClassName, keyStoretype, keyStorePassword);

    }

    public static KeyStore loadKeyStoreFromClasspath(String KeyStoreFileName, String providerClassName, String keyStoretype, String keyStorePassword) throws EncryptionException {

        KeyStore keyStore = null;
        InputStream keyStoreIS = null;

        try {
            EncryptionUtil.setupProvider(providerClassName);
        }
        catch (Exception ex) {
            logger.error("Could not instantiate JCE Provider->" + providerClassName + ":" + ex.toString());
            logger.error("Could not instantiate JCE Provider->" + providerClassName, ex);
        }

        keyStoreIS = EncryptionUtil.class.getClassLoader().getResourceAsStream(KeyStoreFileName);

        if (keyStoreIS != null) {
            try {
                keyStore = KeyStore.getInstance(keyStoretype);
                keyStore.load(keyStoreIS, keyStorePassword.toCharArray());
            }
            catch (Exception ex) {
            	try {
            		logger.error("Failed to load keystore file" + ":storeType:" + keyStoretype + ":keystore file:" + KeyStoreFileName + " " + ex.toString(), ex);
					logger.error("Failed to load keystore from stream" + ":keyStoreIS.available():" + keyStoreIS.available(), ex);
				} 
            	catch (IOException e) {
            		//Already in error block so ignore
				}

            	throw new EncryptionException(ex);
            }
            finally {
                if (keyStoreIS != null) {
                    try {
						keyStoreIS.close();
					} 
                    catch (IOException ex) {
		            	throw new EncryptionException(ex);
					}
                }
            }
        }

        return keyStore;
    }

    public static KeyStore loadKeyStorefromStream(InputStream keyStoreIS, String providerClassName, String keyStoretype, String keyStorePassword) throws EncryptionException {

        KeyStore keyStore = null;
        String keyAlias = "";

        try {
            EncryptionUtil.setupProvider(providerClassName);
        }
        catch (Exception ex) {
        	logger.error("Could not instantiate JCE Provider->" + providerClassName + ":" + ex.toString());
        	logger.error("Could not instantiate JCE Provider->" + providerClassName, ex);
        }

        if (keyStoreIS != null) {
            try {
                keyStore = KeyStore.getInstance(keyStoretype);
                keyStore.load(keyStoreIS, keyStorePassword.toCharArray());
            }
            catch (Exception ex) {
            	try {
					logger.error("Failed to load keystore from stream" + ":keyStoreIS.isNull():" + keyStoreIS + ":providerClassName:" + providerClassName + ":storeType:" + (keyStoretype == null) + ":keyAlias:" + keyAlias + " " + ex.toString(), ex);
					logger.error("Failed to load keystore from stream" + ":keyStoreIS.available():" + keyStoreIS.available(), ex);
				} 
            	catch (IOException e) {
            		//Already in error block so ignore
				}

            	throw new EncryptionException(ex);
            }
            finally {
                if (keyStoreIS != null) {
                    try {
						keyStoreIS.close();
					} 
                    catch (IOException ex) {
		            	throw new EncryptionException(ex);
					}
                }
            }
        }

        return keyStore;

    }    
    
}
